#!/bin/sh /etc/rc.common
# shellcheck disable=SC2034,SC3043,SC1091,SC2155,SC3020,SC3010,SC2016

START=99
STOP=99

USE_PROCD=1

EXTRA_COMMANDS="check_version update auto_setup expand_config auto_setup_noninteractive validate_custom_rules health_check"
EXTRA_HELP="        check_version   Check for updates
        update          Update qosmate
        auto_setup      Automatically configure qosmate
        expand_config   Expand the configuration with all possible options
        auto_setup_noninteractive   Automatically configure qosmate with no interaction
        validate_custom_rules    Validate custom rules
        health_check    Check if QoSmate is properly configured and running"

REQUIRED_PACKAGES="kmod-sched ip-full kmod-veth tc-full kmod-netem kmod-sched-ctinfo kmod-ifb kmod-sched-cake kmod-sched-red luci-lib-jsonc lua jq"

# Base URL (raw) for repositories (can be overridden for different branches/commits)
QOSMATE_BASE_URL="${QOSMATE_BASE_URL:-https://raw.githubusercontent.com/hudra0/qosmate/main}"
QOSMATE_FRONTEND_BASE_URL="${QOSMATE_FRONTEND_BASE_URL:-https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main}"
QOSMATE_FRONTEND_URL_JS="$QOSMATE_FRONTEND_BASE_URL/htdocs/luci-static/resources/view"
QOSMATE_FRONTEND_PATH_JS=/www/luci-static/resources/view/qosmate

QOSMATE_JS_FILES="settings hfsc cake advanced rules connections custom_rules ipsets statistics"

load_config() {
    config_load 'qosmate'
    config_get WAN settings 'WAN' 'eth1'
}

validate_custom_rules() {
    local tmp_file="/tmp/qosmate_custom_rules_validation.txt"
    local rules_file="/etc/qosmate.d/custom_rules.nft"

    if [ ! -f "$rules_file" ]; then
        echo "Custom rules file not found." | tee "$tmp_file"
        return 1
    fi

    nft_output=$(nft --check --file "$rules_file" 2>&1)
    nft_exit_code=$?

    echo "$nft_output" | tee "$tmp_file"

    if [ $nft_exit_code -eq 0 ]; then
        echo "Custom rules validation successful." | tee -a "$tmp_file"
        return 0
    else
        echo "Custom rules validation failed." | tee -a "$tmp_file"
        return 1
    fi
}

# Detect package manager (opkg or apk)
detect_package_manager() {
    local has_apk=0 has_opkg=0
    
    command -v apk >/dev/null 2>&1 && has_apk=1
    command -v opkg >/dev/null 2>&1 && has_opkg=1
    
    if [ $has_apk -eq 1 ] && [ $has_opkg -eq 1 ]; then
        echo "conflict"
    elif [ $has_apk -eq 1 ]; then
        echo "apk"
    elif [ $has_opkg -eq 1 ]; then
        echo "opkg"
    else 
        echo "unknown"
    fi
}

# Check for valid package manager
check_pkg_manager() {
    case "$1" in
        apk|opkg)
            return 0
            ;;
        conflict)
            logger -s -t qosmate -p user.err "Multiple package managers detected (apk and opkg). Package management disabled."
            return 1
            ;;
        *)
            logger -s -t qosmate -p user.err "Error: No supported package manager found."
            return 1
            ;;
    esac
}

# Check if a package is installed
check_package() {
    local pkg="$1"
    
    case "$pkg_manager" in
        "apk")
            apk list -I "$pkg" 2>/dev/null | grep -q "^$pkg-[0-9]"
            return $?
            ;;
        "opkg")
            opkg list-installed | grep -q "^$pkg "
            return $?
            ;;
        *)
            return 1
            ;;
    esac
}

install_packages() {
    # Flag to indicate if opkg update is needed    
    local need_update=0

    check_pkg_manager "$pkg_manager" || return 1

    # Check if any packages are missing
    for pkg in $REQUIRED_PACKAGES; do
        if ! check_package "$pkg"; then
            echo "$pkg is not installed."
            need_update=1
            break
        fi
    done

    # Run update if at least one package is missing
    if [ "$need_update" -eq 1 ]; then
        echo "Updating package list..."
        case "$pkg_manager" in
            "apk")
                apk update
                ;;
            "opkg")
                opkg update
                ;;
        esac

        # Install missing packages
        for pkg in $REQUIRED_PACKAGES; do
            if ! check_package "$pkg"; then
                echo "Installing $pkg..."
                logger -t qosmate "Installing $pkg..."
                case "$pkg_manager" in
                    "apk")
                        apk add "$pkg" || {
                            echo "Failed to install $pkg."
                            logger -t qosmate "Failed to install $pkg."
                            return 1  # Abort if the installation fails
                        }
                        ;;
                    "opkg")
                        opkg install "$pkg" || {
                            echo "Failed to install $pkg."
                            logger -t qosmate "Failed to install $pkg."
                            return 1  # Abort if the installation fails
                        }
                        ;;
                esac
            fi
        done
    fi
}

check_package_status() {
    local missing_packages=""
    
    for pkg in $REQUIRED_PACKAGES; do
        if ! check_package "$pkg"; then
            missing_packages="$missing_packages $pkg"
        fi
    done
    
    if [ -n "$missing_packages" ]; then
        echo "Missing packages:$missing_packages"
        logger -t qosmate "Missing packages:$missing_packages"
        return 1
    fi
    return 0
}

download_scripts() {
    # Download backend files if missing
    [ ! -f /etc/qosmate.sh ] && wget -O /etc/qosmate.sh "$QOSMATE_BASE_URL/etc/qosmate.sh" && chmod a+x /etc/qosmate.sh
    [ ! -f /etc/init.d/qosmate ] && wget -O /etc/init.d/qosmate "$QOSMATE_BASE_URL/etc/init.d/qosmate" && chmod a+x /etc/init.d/qosmate
    
    # Download frontend files if missing
    local frontend_updated=0
    [ ! -f /usr/share/luci/menu.d/luci-app-qosmate.json ] && wget -O /usr/share/luci/menu.d/luci-app-qosmate.json "$QOSMATE_FRONTEND_BASE_URL/root/usr/share/luci/menu.d/luci-app-qosmate.json" && frontend_updated=1
    [ ! -f /usr/share/rpcd/acl.d/luci-app-qosmate.json ] && wget -O /usr/share/rpcd/acl.d/luci-app-qosmate.json "$QOSMATE_FRONTEND_BASE_URL/root/usr/share/rpcd/acl.d/luci-app-qosmate.json" && frontend_updated=1
    [ ! -f /usr/libexec/rpcd/luci.qosmate ] && wget -O /usr/libexec/rpcd/luci.qosmate "$QOSMATE_FRONTEND_BASE_URL/root/usr/libexec/rpcd/luci.qosmate" && chmod a+x /usr/libexec/rpcd/luci.qosmate && frontend_updated=1
    [ ! -f /usr/libexec/rpcd/luci.qosmate_stats ] && wget -O /usr/libexec/rpcd/luci.qosmate_stats "$QOSMATE_FRONTEND_BASE_URL/root/usr/libexec/rpcd/luci.qosmate_stats" && chmod a+x /usr/libexec/rpcd/luci.qosmate_stats && frontend_updated=1

    # Download JS files if missing
    local js_file
    for js_file in $QOSMATE_JS_FILES; do
        [ ! -f "/www/luci-static/resources/view/qosmate/${js_file}.js" ] && wget -O "/www/luci-static/resources/view/qosmate/${js_file}.js" "$QOSMATE_FRONTEND_URL_JS/${js_file}.js" && frontend_updated=1
    done
    
    # Restart rpcd only if any frontend files were updated
    [ "$frontend_updated" -eq 1 ] && /etc/init.d/rpcd restart

    # Create the directory for tc-Netem distributions if it does not exist
    mkdir -p /usr/lib/tc

    # Base URL for the repository
    DIST_URL="$QOSMATE_BASE_URL/usr/lib/tc"

    # List of files
    DIST_FILES="experimental.dist normal.dist normmix20-64.dist pareto.dist paretonormal.dist"

    # Iterate through the list and download each file
    for FILE in $DIST_FILES; do
        if [ ! -f "/usr/lib/tc/$FILE" ]; then
            echo "Downloading $FILE..."
            wget -O "/usr/lib/tc/$FILE" "$DIST_URL/$FILE" || echo "Error downloading $FILE"
        fi
    done
}

create_hotplug_script() {
    cat > /etc/hotplug.d/iface/13-qosmateHotplug << 'EOF'
#!/bin/sh

[ -n "$DEVICE" ] || exit 0
if [ "$ACTION" = ifup ]; then
    . /lib/functions.sh
    config_load qosmate || {
        logger -t qosmate -p user.err "Failed to load config."
        exit 1
    }
    config_get qosmate_enabled global enabled
    if [ "$qosmate_enabled" = "1" ]; then
        logger -t qosmate "Reloading qosmate.sh due to $ACTION of $INTERFACE ($DEVICE)"
        /etc/init.d/qosmate enable
        /etc/init.d/qosmate restart
    else
        logger -t qosmate "qosmate is disabled in the configuration. Not executing the script."
    fi
fi
EOF
}

check_and_download_config() {
    local config_path="/etc/config/qosmate"
    if [ ! -f "$config_path" ]; then
        echo "Configuration file not found, downloading the latest version..."
        wget -O $config_path "$QOSMATE_BASE_URL/etc/config/qosmate" || {
            echo "Error downloading configuration. Please check your internet connection and try again."
            return 1
        }
        echo "Configuration file downloaded successfully."
    else
        echo "Configuration file already exists."
    fi
}

migrate_config() {
    CONFIG_FILE="/etc/config/qosmate"

    # Function to add the global enabled option if it does not exist
    if ! grep -q "config global 'global'" $CONFIG_FILE; then
        echo "Adding global configuration section..."
        sed -i '1i\
config global '\''global'\''\n    option enabled '\''1'\''\n' $CONFIG_FILE
        echo "Global configuration section added."
    else
        echo "Global configuration section already exists."
    fi

    # Ensure the enabled option is present in the global section
    if ! grep -q "option enabled" $CONFIG_FILE; then
        echo "Adding enabled option to global section..."
        sed -i "/config global 'global'/a\\
    option enabled '1'
" $CONFIG_FILE
        echo "Enabled option added."
    else
        echo "Enabled option already exists."
    fi

    # Check for and correct the custom_rules section
    if grep -q "config qosmate 'custom_rules'" $CONFIG_FILE; then
        echo "Incorrect custom_rules section found. Correcting..."
        sed -i "s/config qosmate 'custom_rules'/config custom_rules 'custom_rules'/" $CONFIG_FILE
        echo "custom_rules section corrected."
    fi

    # Remove duplicate custom_rules sections if both incorrect and correct versions exist
    if grep -q "config qosmate 'custom_rules'" $CONFIG_FILE && grep -q "config custom_rules 'custom_rules'" $CONFIG_FILE; then
        echo "Both incorrect and correct custom_rules sections found. Removing the incorrect one..."
        sed -i "/config qosmate 'custom_rules'/d" $CONFIG_FILE
        echo "Incorrect custom_rules section removed."
    fi
}

manage_custom_rules_file() {
    local action="$1"
    
    case "$action" in
        create)
            # Ensure the directory exists
            [ ! -d "/etc/qosmate.d" ] && mkdir -p "/etc/qosmate.d"
            
            # Create the file if it doesn't exist
            [ ! -f "/etc/qosmate.d/custom_rules.nft" ] && touch "/etc/qosmate.d/custom_rules.nft"

            ;;
        delete)
            # Not used at the moment...
            rm -f "/etc/qosmate.d/custom_rules.nft"
            ;;
    esac
}

start_service() {
    install_packages
    download_scripts
    create_hotplug_script  # Create the hotplug script instead of downloading it
    check_and_download_config || return 1
    migrate_config

    # Enable the global option
    uci set qosmate.global.enabled='1'
    uci commit qosmate

    # Save the current WAN interface to a temporary file
    load_config
    echo "$WAN" > /tmp/qosmate_wan

    # Create custom rules file if it doesn't exist
    manage_custom_rules_file create
    nft delete table inet qosmate_custom 2>/dev/null
    nft -f /etc/qosmate.d/custom_rules.nft

    /etc/qosmate.sh
    /etc/init.d/firewall reload
    logger -t qosmate "Service started"
    /etc/init.d/qosmate enable
}

stop_service() {
    # Read the old WAN interface from the temporary file
    OLD_WAN=$(cat /tmp/qosmate_wan 2>/dev/null)
    if [ -z "$OLD_WAN" ]; then
        # If the temporary file doesn't exist, fall back to WAN from config
        load_config
        OLD_WAN="$WAN"
    fi

    echo "Stopping service qosmate..."
    
    # Only disable if not in shutdown and not in restart
    if [ "$DISABLE_ON_STOP" != "0" ] && [ -z "$FROM_SHUTDOWN" ]; then
        /etc/init.d/qosmate disable
        uci set qosmate.global.enabled='0'
        uci commit qosmate
    fi

    # Remove custom rules table
    nft delete table inet qosmate_custom 2>/dev/null

    ## Delete files
    rm -f /etc/hotplug.d/iface/13-qosmateHotplug
    rm -f /usr/share/nftables.d/ruleset-post/dscptag.nft

    ## Delete the old qdiscs and IFB associated with the old WAN interface
    tc qdisc del dev "$OLD_WAN" root > /dev/null 2>&1
    tc qdisc del dev ifb-"$OLD_WAN" root > /dev/null 2>&1
    tc qdisc del dev "$OLD_WAN" ingress > /dev/null 2>&1

    # Remove IFB interface
    ip link del ifb-"$OLD_WAN" 2>/dev/null

    nft delete table inet dscptag

    # Remove the temporary file
    rm -f /tmp/qosmate_wan

    echo "Reloading network service..."
    /etc/init.d/network reload
    /etc/init.d/firewall reload
    logger -t qosmate "Service stopped"
    exit 0
}

shutdown() {
    # Set variable for shutdown
    FROM_SHUTDOWN=1
    # Call stop_service
    stop_service
}

status_service() {
    # Load configuration
    config_load 'qosmate'

    # Function to extract default values from the main script
    get_default_value() {
        local var_name="$1"
        grep "^$var_name=" /etc/qosmate.sh | cut -d'=' -f2- | tr -d '"'
    }

    # Get current values, replace with defaults if not set
    local WAN UPRATE DOWNRATE GAMEUP GAMEDOWN gameqdisc
    config_get WAN settings WAN
    : "${WAN:="$(get_default_value DEFAULT_WAN)"}"
    config_get UPRATE settings UPRATE
    : "${UPRATE:="$(get_default_value DEFAULT_UPRATE)"}"
    config_get DOWNRATE settings DOWNRATE
    : "${DOWNRATE:="$(get_default_value DEFAULT_DOWNRATE)"}"

    config_get ROOT_QDISC settings ROOT_QDISC "hfsc"
    config_get gameqdisc hfsc gameqdisc
    : "${gameqdisc:="$(get_default_value DEFAULT_GAMEQDISC)"}"

    # For default GAMEUP and GAMEDOWN, we need to evaluate the expressions
    config_get GAMEUP hfsc GAMEUP
    : "${GAMEUP:=$((UPRATE*15/100+400))}"
    config_get GAMEDOWN hfsc GAMEDOWN
    : "${GAMEDOWN:=$((DOWNRATE*15/100+400))}"

    echo "==== qosmate Status ===="
    
    # Check if autostart is enabled
    if /etc/init.d/qosmate enabled; then
        echo "qosmate autostart is enabled."
    else
        echo "qosmate autostart is not enabled."
    fi

    # Check if the service is enabled in UCI config
    if [ "$(uci -q get qosmate.global.enabled)" = "1" ]; then
        echo "qosmate service is enabled."
    else
        echo "qosmate service is disabled."
    fi

    # Check if traffic shaping is active
    local IFB="ifb-$WAN"
    
    if tc qdisc show dev "$WAN" 2>/dev/null | grep -q "qdisc cake"; then
        if [ "$(uci -q get qosmate.global.enabled)" = "1" ]; then
            echo "Traffic shaping is active on the egress interface ($WAN)."
        else
            echo "Default CAKE qdisc is active on the egress interface ($WAN), but qosmate is not managing it."
        fi
    elif tc qdisc show dev "$WAN" 2>/dev/null | grep -q "qdisc hfsc"; then
        echo "Traffic shaping (HFSC) is active on the egress interface ($WAN)."
    else
        echo "No traffic shaping is active on the egress interface ($WAN)."
    fi

    if tc qdisc show dev "$IFB" 2>/dev/null | grep -q "qdisc cake\|qdisc hfsc"; then
        echo "Traffic shaping is active on the ingress interface ($IFB)."
    else
        echo "No traffic shaping is active on the ingress interface ($IFB)."
    fi

    echo "==== Overall Status ===="
    # Determine if the service is actually running
    if [ "$(uci -q get qosmate.global.enabled)" = "1" ] && { tc qdisc show dev "$WAN" 2>/dev/null | grep -q "qdisc hfsc" ||
            tc qdisc show dev "$IFB" 2>/dev/null | grep -q "qdisc cake\|qdisc hfsc"; }; then
            echo "qosmate is currently active and managing traffic shaping."
    else
        echo "qosmate is not currently active or managing traffic shaping."
    fi

    echo "==== Current Settings ===="
    # Show summary of current settings
    echo "Upload rate: $UPRATE kbps"
    echo "Download rate: $DOWNRATE kbps"
    
    echo "Game traffic upload: $GAMEUP kbps"
    echo "Game traffic download: $GAMEDOWN kbps"
    if [ "$ROOT_QDISC" = "cake" ]; then
        echo "Queue discipline: CAKE (Root qdisc)"
    else
        echo "Queue discipline: $gameqdisc (for game traffic in HFSC)"
    fi

    echo "==== Package Status ===="

    if check_pkg_manager "$pkg_manager" && check_package_status; then
        echo "All required packages are installed."
    else
        echo "Some required packages are missing. QoSmate may not function correctly."
    fi

    echo
    echo "==== Detailed Technical Information ===="
    echo "Traffic Control (tc) Queues:"
    tc -s qdisc

    echo
    echo "==== Nftables Ruleset (dscptag) ===="
    nft list ruleset | grep 'chain dscptag' -A 100


    echo
    echo "==== Custom Rules Table Status ===="
    if nft list table inet qosmate_custom &>/dev/null; then
        echo "Custom rules table (qosmate_custom) is active."
        echo "Current custom rules:"
        nft list table inet qosmate_custom
    else
        echo "Custom rules table (qosmate_custom) is not active or doesn't exist."
    fi

}

restart() {
    DISABLE_ON_STOP=0 /etc/init.d/qosmate stop
    sleep 1 # Ensure all processes have been properly terminated
    /etc/init.d/qosmate start    
}

reload_service() {
    restart
}

# Extract version from local or remote file
# 1 - component (backend|frontend)
# 2 - origin (local|remote)
# Error codes: 1 - failed to get version, 2 - got invalid version
get_version() {
    local component="$1" origin="$2"
    local fetch_cmd fetch_path pattern FS rv backend_path_prefix frontend_path_prefix

    case "$origin" in
        local)
            fetch_cmd="cat"
            backend_path_prefix=
            frontend_path_prefix="$QOSMATE_FRONTEND_PATH_JS"
            ;;
        remote)
            fetch_cmd="wget -qO-"
            backend_path_prefix="$QOSMATE_BASE_URL"
            frontend_path_prefix="$QOSMATE_FRONTEND_URL_JS"
    esac

    case "$component" in
        backend)
            fetch_path="${backend_path_prefix}/etc/qosmate.sh"
            pattern="^VERSION="
            FS='"'
            ;;
        frontend)
            fetch_path="${frontend_path_prefix}/settings.js"
            pattern="^const UI_VERSION"
            FS="'"
    esac

    eval "$fetch_cmd" "$fetch_path" | {
        # assumes version string enclosed in FS and no prior FS present in the version line
        awk -F "$FS" "BEGIN{rv=1} \$0~/$pattern/ {rv=2; print \$2; if (\$2~/^[0-9]+([.][0-9]+)*$/) {rv=0}; exit} END{exit rv}"
        rv=$?
        cat > /dev/null # sink extra lines, otherwise this will be extremely slow
        exit $rv # sets the exit code of the pipe
    }
    return $?
}

# Check and print versions for local and remote backend and frontend
# sets global variables: $backend_upd_avail, $frontend_upd_avail
# 1 - (optional) '-n' to not print update tip
# Return codes:
# 0 - no update
# 1 - error
# 254 - update available
check_version() {
    local origin notify_origin component notify_component version local_version upd_avail=''
    local no_print_tip="$1"

    for component in backend frontend; do
        case "$component" in
            backend)
                notify_component=Backend
                ;;
            frontend)
                notify_component=Frontend
        esac

        echo "$notify_component versions:"

        for origin in local remote; do
            case "$origin" in
                local)
                    notify_origin=Current
                    ;;
                remote)
                    notify_origin=Latest
            esac

            version="$(get_version "$component" "$origin")" || {
                if [ "$?" = 2 ] && [ -n "$version" ]; then
                    printf '%s\n' "Error: Got invalid $origin $component version '$version'." >&2
                else
                    printf '%s\n' "Error: Failed to get $origin $component version." >&2
                fi
                return 1
            }

            case "$origin" in
                local)
                    local_version="$version"
                    ;;
                remote)
                    if [ "$version" = "$local_version" ]; then
                        unset "${component}_upd_avail"
                    else
                        upd_avail=1
                        eval "${component}_upd_avail=1"
                    fi
            esac
            printf '%s\n' "  $notify_origin version: $version"
        done
    done

    if [ -n "$upd_avail" ]; then
        [ "$no_print_tip" != '-n' ] && printf '\n%s\n%s\n' "A new version of QoSmate is available." \
            "To update, run: /etc/init.d/qosmate update"
        return 254
    else
        printf '\n%s\n' "QoSmate is up to date."
    fi
    :
}

# Ignores versions when global var $QOSMATE_FORCE is set to 1
update() {
    local component upd_avail=''

    check_version -n
    case $? in
        0) ;;
        254)
            upd_avail=1
            ;;
        *)
            return 1
            ;;
    esac

    if [ "$QOSMATE_FORCE" = 1 ]; then
        echo "Force update enabled. Proceeding with update even if versions are identical."
    elif [ -z "$upd_avail" ]; then
        return 0
    fi

    echo "Updates available. Do you want to update? [y/N] "
    read -r answer
    case "$answer" in
        y|Y) ;;
        *)
            echo "Update cancelled."
            return 0
    esac

    echo "Updating QoSmate..."

    for component in backend frontend; do
        local component_upd_avail js_file js_files gen_file gen_files exec_files exec_file fetch_base_url notify_component

        case "$component" in
            backend)
                notify_component=Backend
                fetch_base_url="${QOSMATE_BASE_URL}"
                js_files=
                gen_files=" \
                    /etc/qosmate.sh \
                    /etc/init.d/qosmate \
                    /etc/hotplug.d/iface/13-qosmateHotplug"
                exec_files=" \
                    /etc/qosmate.sh \
                    /etc/init.d/qosmate"
                ;;
            frontend)
                notify_component=Frontend
                fetch_base_url="${QOSMATE_FRONTEND_BASE_URL}/root"
                js_files="$QOSMATE_JS_FILES"
                gen_files=" \
                    /usr/share/luci/menu.d/luci-app-qosmate.json \
                    /usr/share/rpcd/acl.d/luci-app-qosmate.json \
                    /usr/libexec/rpcd/luci.qosmate \
                    /usr/libexec/rpcd/luci.qosmate_stats"
                exec_files="/usr/libexec/rpcd/luci.qosmate \
                    /usr/libexec/rpcd/luci.qosmate_stats"
        esac

        eval component_upd_avail="\"\${${component}_upd_avail}\""
        if [ "$QOSMATE_FORCE" = 1 ] || [ -n "$component_upd_avail" ]; then
            printf '\n%s\n' "Updating $component..."

            # !! This relies on remote file path suffix matching local file path
            for gen_file in $gen_files; do
                wget -O "$gen_file" "${fetch_base_url}${gen_file}" || {
                    echo "Command 'wget -O \"$gen_file\" \"${fetch_base_url}${gen_file}\"' exited with code $?." >&2
                    echo "$notify_component update failed." >&2
                    return 1
                }
            done

            for exec_file in $exec_files; do
                chmod +x "$exec_file" || {
                    echo "Failed to make file '$exec_file' executable." >&2
                    return 1
                }
            done

            if [ "$component" = frontend ]; then
                for js_file in $js_files; do
                    wget -O "$QOSMATE_FRONTEND_PATH_JS/$js_file.js" "$QOSMATE_FRONTEND_URL_JS/$js_file.js" || {
                        echo "Command 'wget -O \"$QOSMATE_FRONTEND_PATH_JS/$js_file.js\" \"$QOSMATE_FRONTEND_URL_JS/$js_file.js\"' exited with code $?." >&2
                        echo "$notify_component update failed." >&2
                        return 1
                    }
                done

                /etc/init.d/rpcd restart
                /etc/init.d/uhttpd restart
            fi

            echo "$notify_component update complete."
        fi
    done

    expand_config

    echo "Update complete. Please restart QoSmate."
    /etc/init.d/qosmate restart
}

# 1 - speedtest command
# 2 - Upload|Download
# I/O via STDIN/STDOUT
parse_speedtest_output() {
    local speedtest_cmd="$1" direction="$2"
    case "$speedtest_cmd" in
        speedtest-go*)
            grep "${direction}:" | grep -oE '[0-9]+\.[0-9]+' | head -n1
            ;;
        "speedtest --simple"*)
            # match to (Upload:|Download:).*, print field 2. if no match, return 1
            awk "BEGIN{rv=1} \$0~/$direction:/ {print \$2; rv=0; exit} END{exit rv}"
            ;;
        *)
            echo "Unexpected speedtest command '$speedtest_cmd'." >&2
            false
            ;;
    esac || {
        echo "Failed to get the $direction speed." >&2
        echo "0"
        return 1
    }
    :
}

# for non-interactive setup, call with '-n [gaming_ip_address]'
auto_setup() {
    local L3_DEVICE WAN_INTERFACE FINAL_INTERFACE SPEEDTEST_CMD FREE_SPACE SPEED_RESULT DOWNLOAD_SPEED UPLOAD_SPEED
    local gaming_ip='' noninteractive='' speed_choice response speedtest_req direction

    if [ "$1" = "-n" ]; then
        gaming_ip="$2"
        noninteractive=1
    else
        noninteractive=
    fi
    [ -z "$noninteractive" ] && echo "Starting qosmate auto-setup..."

    # Stop qosmate if it's running
    if /etc/init.d/qosmate status > /dev/null; then
        echo "Stopping qosmate for accurate speed test results..."
        /etc/init.d/qosmate stop
        sleep 5  # Give some time for the network to stabilize
    fi

    # Detect WAN interface
    WAN_INTERFACE=$(ifstatus wan | grep -e '"device"' | cut -d'"' -f4)
    L3_DEVICE=$(ifstatus wan | grep -e '"l3_device"' | cut -d'"' -f4)

    if [ -z "$WAN_INTERFACE" ] && [ -z "$L3_DEVICE" ]; then
        echo "Error: Unable to detect WAN interface. Please set it manually in the configuration."
        return 1
    fi

    FINAL_INTERFACE=${L3_DEVICE:-$WAN_INTERFACE}
    echo "Detected WAN interface: $FINAL_INTERFACE"

    if [ -n "$noninteractive" ]; then
        speedtest_req=1
    else
        while :; do
            echo "Do you want to run a speed test or enter speeds manually? [test/manual]"
            read -r speed_choice
            case "$speed_choice" in
                *[A-Z]*) speed_choice="$(printf %s "$speed_choice" | tr 'A-Z' 'a-z')"
            esac

            case "$speed_choice" in
                test|manual) break
            esac
            echo "Invalid input '$speed_choice'. Please enter 'test' or 'manual'."
        done

        if [[ "$speed_choice" = manual ]]; then
            for direction in download upload; do
                while :; do
                    echo "Please enter your $direction speed in Mbit/s:"
                    read -r response
                    case "$response" in
                        ''|*[!0-9.]*|*.*.*)
                            # do not allow empty string or irrelevant characters or 2x '.'
                            echo "Invalid input '$response'. Please try again."
                            continue
                            ;;
                        *[0-9]*)
                            break
                            ;;
                        *)
                            # do not allow input without digits
                            echo "Invalid input '$response'. Please try again."
                            continue
                            ;;
                    esac
                done
                case "$direction" in
                    download) DOWNLOAD_SPEED="$response" ;;
                    upload) UPLOAD_SPEED="$response" ;;
                esac
            done
            speedtest_req=
        else
            echo "This will run a speed test to configure qosmate. Do you want to continue? [y/N]"
            read -r response
            if [[ ! "$response" =~ ^[Yy]$ ]]; then
                echo "Auto-setup cancelled."
                return 0
            fi
            speedtest_req=1
        fi
    fi

    if [ -n "$speedtest_req" ]; then
        # Check for speedtest-go first
        if command -v speedtest-go &> /dev/null; then
            echo "speedtest-go is already installed. Using it for the speed test."
            SPEEDTEST_CMD="speedtest-go"
        else
            echo "speedtest-go is not found. Checking for python3-speedtest-cli..."
            if command -v speedtest &> /dev/null; then
                echo "python3-speedtest-cli is already installed. Using it for the speed test."
                SPEEDTEST_CMD="speedtest --simple"
            else
                echo "Neither speedtest-go nor python3-speedtest-cli is installed. Attempting to install speedtest-go..."
                check_pkg_manager "$pkg_manager" || return 1
                
                # Check for sufficient space (adjust the required space as needed)
                FREE_SPACE=$(df /overlay | awk 'NR==2 {print $4}')
                if [ "$FREE_SPACE" -lt 15360 ]; then  # Assuming 15MB for speedtest-go
                    echo "Not enough space for speedtest-go. Attempting to install python3-speedtest-cli instead..."
                else
                    case "$pkg_manager" in
                        "apk")
                            apk update && apk add speedtest-go
                            ;;
                        "opkg")
                            opkg update && opkg install speedtest-go
                            ;;
                    esac
                    if [ $? -eq 0 ]; then
                        SPEEDTEST_CMD="speedtest-go"
                    else
                        echo "Failed to install speedtest-go. Attempting to install python3-speedtest-cli instead..."
                    fi
                fi

                # If speedtest-go installation failed or there wasn't enough space, try python3-speedtest-cli
                if [ -z "$SPEEDTEST_CMD" ]; then
                    if [ "$FREE_SPACE" -lt 1024 ]; then  # 1MB for python3-speedtest-cli
                        echo "Error: Not enough free space to install any speedtest tool."
                        echo "Auto-setup cannot continue. Please free up some space and try again."
                        return 1
                    fi
                    case "$pkg_manager" in
                        "apk")
                            apk update && apk add python3-speedtest-cli
                            ;;
                        "opkg")
                            opkg update && opkg install python3-speedtest-cli python3-speedtest-cli-src
                            ;;
                    esac
                    if [ $? -eq 0 ]; then
                        SPEEDTEST_CMD="speedtest --simple"
                    else
                        echo "Failed to install python3-speedtest-cli. Auto-setup cannot continue."
                        return 1
                    fi
                fi
            fi
        fi

        echo "Running speed test... This may take a few minutes."
        SPEED_RESULT=$($SPEEDTEST_CMD)

        DOWNLOAD_SPEED=$(printf %s "$SPEED_RESULT" | parse_speedtest_output "$SPEEDTEST_CMD" Download) &&
        UPLOAD_SPEED=$(printf %s "$SPEED_RESULT" | parse_speedtest_output "$SPEEDTEST_CMD" Upload) ||
            return 1

        echo "Speed test results:"
        echo "Download speed: $DOWNLOAD_SPEED Mbit/s"
        echo "Upload speed: $UPLOAD_SPEED Mbit/s"
    fi

    # Convert speeds to kbps and apply 90% rule
    DOWNRATE=$(awk -v speed="$DOWNLOAD_SPEED" 'BEGIN {print int(speed * 1000 * 0.9)}')
    UPRATE=$(awk -v speed="$UPLOAD_SPEED" 'BEGIN {print int(speed * 1000 * 0.9)}')

    echo "QoS configuration:"
    echo "DOWNRATE: $DOWNRATE kbps (90% of measured download speed)"
    echo "UPRATE: $UPRATE kbps (90% of measured upload speed)"

    # Section for gaming device IP
    if [ -z "$noninteractive" ]; then
        echo "Would you like to add a gaming device IP for prioritization? [y/N]"
        read -r response
        if [[ "$response" =~ ^[Yy]$ ]]; then
            echo "Please enter the IP address of your gaming device:"
            read -r gaming_ip
        fi
    fi

    if [ -n "$gaming_ip" ]; then
        # Validate IP address format
        if [[ $gaming_ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
            # Check if rules for this IP already exist
            if grep -q "option src_ip '$gaming_ip'" /etc/config/qosmate || grep -q "option dest_ip '$gaming_ip'" /etc/config/qosmate; then
                echo "Rules for IP $gaming_ip already exist. Skipping addition of new rules."
                gaming_ip=
            fi
        else
            echo "Invalid IP address format. No gaming device rules added."
            gaming_ip=
        fi
    fi

    local temp_config_file="/tmp/qosmate_config.tmp"
    if [ -f /etc/config/qosmate ]; then
        cp /etc/config/qosmate "$temp_config_file"
    else
        echo "Error: config file '/etc/config/qosmate' not found."
        return 1
    fi

    # TODO: all of this should be rewritten with UCI commands
    sed -i "/option WAN/c\    option WAN '$FINAL_INTERFACE'
        /option DOWNRATE/c\    option DOWNRATE '$DOWNRATE'
        /option UPRATE/c\    option UPRATE '$UPRATE'" \
            "$temp_config_file" &&
    if [ -n "$gaming_ip" ] && cat << EOF >> "$temp_config_file"
config rule
    option name 'Game_Console_Outbound'
    option proto 'udp'
    option src_ip '$gaming_ip'
    list dest_port '!=80'
    list dest_port '!=443'
    option class 'cs5'
    option counter '1'

config rule
    option name 'Game_Console_Inbound'
    option proto 'udp'
    option dest_ip '$gaming_ip'
    list src_port '!=80'
    list src_port '!=443'
    option class 'cs5'
    option counter '1'
EOF
    then
        echo "Gaming device rules added for IP: $gaming_ip"
        :
    else
        echo "No gaming device IP added."
        :
    fi &&
    mv "$temp_config_file" /etc/config/qosmate ||
    {
        echo "Failed to modify the config. Please check the file /etc/config/qosmate" >&2
        return 1
    }

    echo "Configuration updated. New settings:"
    grep -E "option (WAN|DOWNRATE|UPRATE)" /etc/config/qosmate


    echo "Auto-setup complete. qosmate has been configured with detected settings."
    echo "To apply these changes, please restart qosmate by running: /etc/init.d/qosmate restart"
}

auto_setup_noninteractive() {
    local output_file="/tmp/qosmate_auto_setup_output.txt" auto_setup_rv
    {
        echo "Starting qosmate non-interactive auto-setup..."
        auto_setup -n "$1" 2>&1
    } > "$output_file"
    auto_setup_rv=$?
    echo "$output_file"
    return $auto_setup_rv
}

expand_config() {
    local config_file="/etc/config/qosmate"
    local main_script="/etc/qosmate.sh"
    
    echo "Expanding qosmate configuration with all possible options..."

    # Ensure all sections exist
    for section in global settings advanced hfsc cake custom_rules; do
        if ! grep -q "config $section '$section'" "$config_file"; then
            echo >> "$config_file"  # Add a newline before the new section
            echo "config $section '$section'" >> "$config_file"
            echo "Added section: $section"
        fi
    done

    # Function to get default value from main script
    get_default_value() {
        local option="$1"
        local default_value

        # Map options to their DEFAULT_ counterparts
        case "$option" in
            OH) option="DEFAULT_OH" ;;
            WAN) option="DEFAULT_WAN" ;;
            DOWNRATE) option="DEFAULT_DOWNRATE" ;;
            UPRATE) option="DEFAULT_UPRATE" ;;
        esac

        # First, try to find a direct assignment
        default_value=$(grep "^${option}=" "$main_script" | cut -d'=' -f2- | tr -d '"')

        # If not found, look for it in the load_config function
        if [ -z "$default_value" ]; then
            default_value=$(sed -n "/load_config()/,/}/p" "$main_script" | grep "\<${option}=" | sed 's/.*|| echo "\(.*\)".*/\1/')
        fi

        # Remove any remaining shell expansions and trim whitespace
        default_value=$(echo "$default_value" | sed -e 's/\$([^)]*)//g' -e 's/^ *//g' -e 's/ *$//g' -e 's/\${{.*}}//g')

        # Check for dynamic calculations and return empty string if found
        if echo "$default_value" | grep -qE '[[($]|\)$'; then
            echo ""
        else
            echo "$default_value"
        fi
    }
    # Define options for each section
    add_options() {
        local section="$1"
        shift
        for option in "$@"; do
            if ! grep -q "option $option" "$config_file"; then
                default_value=$(get_default_value "$option")
                sed -i "/config $section/a\\    option $option '$default_value'" "$config_file"
                echo "Added option $option to section $section with value: $default_value"
            fi
        done
    }

    # Add options for each section
    add_options global enabled
    add_options settings WAN DOWNRATE UPRATE ROOT_QDISC
    add_options advanced PRESERVE_CONFIG_FILES WASHDSCPUP WASHDSCPDOWN BWMAXRATIO ACKRATE UDP_RATE_LIMIT_ENABLED UDPBULKPORT TCPBULKPORT VIDCONFPORTS REALTIME4 REALTIME6 LOWPRIOLAN4 LOWPRIOLAN6 TCP_UPGRADE_ENABLED MSS NFT_HOOK NFT_PRIORITY
    add_options hfsc LINKTYPE OH gameqdisc GAMEUP GAMEDOWN nongameqdisc nongameqdiscoptions MAXDEL PFIFOMIN PACKETSIZE netemdelayms netemjitterms netemdist pktlossp
    add_options cake COMMON_LINK_PRESETS OVERHEAD MPU LINK_COMPENSATION ETHER_VLAN_KEYWORD PRIORITY_QUEUE_INGRESS PRIORITY_QUEUE_EGRESS HOST_ISOLATION NAT_INGRESS NAT_EGRESS ACK_FILTER_EGRESS RTT AUTORATE_INGRESS EXTRA_PARAMETERS_INGRESS EXTRA_PARAMETERS_EGRESS

    echo "Configuration expanded successfully. You can now edit all available options in $config_file"
}

health_check() {
    # Check if QoSmate is properly configured and running
    local status=""
    local errors=0
    
    load_config
    
    # Check if the service is enabled
    if /etc/init.d/qosmate enabled; then
        status="${status}service:enabled;"
    else
        status="${status}service:disabled;"
        errors=$((errors + 1))
    fi
    
    # Check nftables configuration
    if nft list table inet dscptag >/dev/null 2>&1; then
        status="${status}nft:ok;"
    else
        status="${status}nft:failed;"
        errors=$((errors + 1))
    fi
    
    # Check tc configuration on WAN interface
    if [ -n "$WAN" ] && tc qdisc show dev "$WAN" 2>/dev/null | grep -E -q "hfsc|cake"; then
        status="${status}tc:ok;"
    else
        status="${status}tc:failed;"
        errors=$((errors + 1))
    fi
    
    # Check if essential config file exists
    if [ -f "/etc/config/qosmate" ]; then
        status="${status}config:ok;"
    else
        status="${status}config:missing;"
        errors=$((errors + 1))
    fi
    
    # Check required packages
    local missing_packages=""
    for pkg in $REQUIRED_PACKAGES; do
        if ! check_package "$pkg"; then
            missing_packages="${missing_packages}${pkg} "
        fi
    done
    
    if [ -z "$missing_packages" ]; then
        status="${status}packages:ok;"
    else
        status="${status}packages:missing:${missing_packages};"
        errors=$((errors + 1))
    fi
    
    # Output status
    echo "status=$status;errors=$errors"
    
    # Return code depending on errors
    [ $errors -eq 0 ] || return 1
    return 0
}

pkg_manager="$(detect_package_manager)" # global variable
